#include "Camera.h"

#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtx/quaternion.hpp>

#include "Walnut/Input/Input.h"

using namespace Walnut;

Camera::Camera(float verticalFOV, float nearClip, float farClip) :
    m_VerticalFOV(verticalFOV), m_NearClip(nearClip), m_FarClip(farClip)
{
	m_ForwardDirection = glm::vec3(0, 0, -1);
	m_Position         = glm::vec3(0, 0, 6);
}
// 
void Camera::OnUpdate(float ts)
{
	glm::vec2 mousePos  = Input::GetMousePosition();
	glm::vec2 delta     = (mousePos - m_LastMousePosition) * 0.002f;
	m_LastMousePosition = mousePos;

	if (!Input::IsMouseButtonDown(MouseButton::Right))
	{
		Input::SetCursorMode(CursorMode::Normal);
		return;
	}

	Input::SetCursorMode(CursorMode::Locked);

	bool moved = false;

	constexpr glm::vec3 upDirection(0.0f, 1.0f, 0.0f);
	glm::vec3           rightDirection = glm::cross(m_ForwardDirection, upDirection);

	float speed = 5.0f;

	// Movement with key controlled direction (attached time and speed) to change m_Position
	if (Input::IsKeyDown(KeyCode::W))
	{
		m_Position += m_ForwardDirection * speed * ts;
		moved = true;
	}
	else if (Input::IsKeyDown(KeyCode::S))
	{
		m_Position -= m_ForwardDirection * speed * ts;
		moved = true;
	}
	if (Input::IsKeyDown(KeyCode::A))
	{
		m_Position -= rightDirection * speed * ts;
		moved = true;
	}
	else if (Input::IsKeyDown(KeyCode::D))
	{
		m_Position += rightDirection * speed * ts;
		moved = true;
	}
	if (Input::IsKeyDown(KeyCode::Q))
	{
		m_Position -= upDirection * speed * ts;
		moved = true;
	}
	else if (Input::IsKeyDown(KeyCode::E))
	{
		m_Position += upDirection * speed * ts;
		moved = true;
	}

	// Rotation with pitch and yaw
	if (delta.x != 0.0f || delta.y != 0.0f)
	{
		float pitchDelta = delta.y * GetRotationSpeed();
		float yawDelta   = delta.x * GetRotationSpeed();

		// NOTE: use queterion to calculate front vector
		glm::quat q        = glm::normalize(glm::cross(glm::angleAxis(-pitchDelta, rightDirection),
		                                               glm::angleAxis(-yawDelta, glm::vec3(0.f, 1.0f, 0.0f))));
		m_ForwardDirection = glm::rotate(q, m_ForwardDirection);

		moved = true;
	}

	if (moved)
	{
		RecalculateView();
		RecalculateRayDirections();
	}
}

void Camera::OnResize(uint32_t width, uint32_t height)
{
	if (width == m_ViewportWidth && height == m_ViewportHeight)
		return;

	m_ViewportWidth  = width;
	m_ViewportHeight = height;

	RecalculateProjection();
	RecalculateRayDirections();
}

float Camera::GetRotationSpeed()
{
	return 0.3f;
}

void Camera::RecalculateProjection()
{
	// NOTE: calculate projection matrix in [perspective] function
	m_Projection = glm::perspectiveFov(glm::radians(m_VerticalFOV), (float) m_ViewportWidth, (float) m_ViewportHeight, m_NearClip, m_FarClip);
	// also calculate inverse projection matrix for mapping back to world space
	m_InverseProjection = glm::inverse(m_Projection);
}

void Camera::RecalculateView()
{
	// NOTE: calculate view matrix with [lookAt] function
	m_View = glm::lookAt(m_Position, m_Position + m_ForwardDirection, glm::vec3(0, 1, 0));
	// also calculate inverse view matrix for mapping back to world space
	m_InverseView = glm::inverse(m_View);
}

// ray direction cached in a list
void Camera::RecalculateRayDirections()
{
	m_RayDirections.resize(m_ViewportWidth * m_ViewportHeight);

	for (uint32_t y = 0; y < m_ViewportHeight; y++)
	{
		for (uint32_t x = 0; x < m_ViewportWidth; x++)
		{
			// get coord u, v
			glm::vec2 coord = {(float) x / (float) m_ViewportWidth, (float) y / (float) m_ViewportHeight};
			coord           = coord * 2.0f - 1.0f;        // -1 -> 1

			// vector form [u,v] to [camera] 
			glm::vec4 target = m_InverseProjection * glm::vec4(coord.x, coord.y, 1, 1);
			//  
            //  vector from [camera] to [u, v]
			glm::vec3 rayDirection                   = glm::vec3(m_InverseView * glm::vec4(glm::normalize(glm::vec3(target) / target.w), 0));        // World space
			m_RayDirections[x + y * m_ViewportWidth] = rayDirection;
		}
	}
}
